package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.SystemConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.SHOP_GEO_KEY;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    private final StringRedisTemplate stringRedisTemplate;
    //    private final RedisBloomFilter shpBloomFilter;
    private final CacheClient cacheClient;
    private final ExecutorService executor = Executors.newFixedThreadPool(10);

    /**
     * 查询商铺信息
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {

//        Shop shop = queryMutexByLogicExpire(id);
        Shop shop = cacheClient.getCacheWithLogicExpire(
                RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.LOCK_SHOP_KEY, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }


    public Shop queryMutexByLogicExpire(Long id) {

        /*if (!shpBloomFilter.contains(id.toString())) {
            return null;
        }*/

        // 1. 从redis查询缓存(需事先预热)
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(key);
        if (map.isEmpty()) {
            // 2.未获取到数据,直接返回
            return null;
        }
        //3.查到缓存,判断是否逻辑过期
        RedisData redisData = BeanUtil.fillBeanWithMap(map, new RedisData(), false);
        Shop shop = JSONUtil.toBean((String) redisData.getData(), Shop.class);
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            //3.1 未过期
            return shop;
        }
        //3.2已过期
        //4 重新构建缓存
        //4.1 获取锁
        boolean tryLock = tryLock(id);
        //4.2 判断是否获取锁成功
        if (tryLock) {
            executor.submit(() -> {
                //4.3 获取锁成功,开启线程重构缓存
                save2Redis(id, RedisConstants.CACHE_SHOP_TTL);
            });
        }
        // 无论是否获取锁,都直接返回过期数据
        return shop;
    }

    /**
     * 保存到redis
     *
     * @param id
     */
    public void save2Redis(Long id, Long expireMinutes) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;

        /*//
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }*/

        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(JSONUtil.toJsonStr(shop));
        redisData.setExpireTime(LocalDateTime.now().plusMinutes(expireMinutes));
        Map<String, Object> map = BeanUtil.beanToMap(redisData, new HashMap<>(),
                CopyOptions.create()
                        .ignoreNullValue()
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue == null ? "" : fieldValue.toString())
        );
        stringRedisTemplate.opsForHash().putAll(key, map);
    }

    /**
     * 通过使用互斥锁解决缓存击穿
     *
     * @param id
     * @return
     */
    public Shop queryMutex(Long id) {

       /* if (!shpBloomFilter.contains(id.toString())) {
            return null;
        }*/
        // 1. 从redis查询缓存
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(key);
        if (!map.isEmpty()) {
            // 2.获取到数据,直接返回
            return BeanUtil.fillBeanWithMap(map, new Shop(), false);
        }
        //3.未查到缓存,获取互斥锁,更新缓存
        try {
            //3.获取锁
            boolean tryLock = tryLock(id);
            //3.2 判断锁是否获取成功
            if (!tryLock) {
                //3.3获取失败,等待并重试
                Thread.sleep(50);
                return queryMutex(id);
            }
            //3.4 获取锁成功,查询数据库
            Shop shop = getById(id);
            if (shop == null) {
                return null;
            }
            //3.5更新缓存
            Map<String, Object> beanedToMap = BeanUtil.beanToMap(shop, new HashMap<>(),
                    CopyOptions.create().ignoreNullValue()
                            .setIgnoreNullValue(true)
                            .setFieldValueEditor((fieldName, fieldValue) -> fieldValue == null ? "" : fieldValue.toString())
            );
            stringRedisTemplate.opsForHash().putAll(key, beanedToMap);
            // 添加过期时间
            stringRedisTemplate.expire(key, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return shop;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //4.释放锁
            unLock(id);
        }

    }

    public Shop queryCacheBreakdown(Long id) {
       /* if (!shpBloomFilter.contains(id.toString())) {
            return null;
        }*/
        // 从redis查询缓存
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(key);
        if (!map.isEmpty()) {
            Shop shop = BeanUtil.fillBeanWithMap(map, new Shop(), false);
            return shop;
        }
        // 未查到缓存,查询数据库
        Shop shop = getById(id);
        if (shop == null) {
            return null;
        }
        // 写入缓存
        Map<String, Object> beanedToMap = BeanUtil.beanToMap(shop, new HashMap<>(),
                CopyOptions.create().ignoreNullValue()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue == null ? "" : fieldValue.toString())
        );
        stringRedisTemplate.opsForHash().putAll(key, beanedToMap);
        // 添加过期时间
        stringRedisTemplate.expire(key, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }

    /**
     * 更新商铺信息
     *
     * @param shop
     */
    @Override
    public void updateShop(Shop shop) {
        // 更新数据库信息
        updateById(shop);

        // 删除缓存
        String key = RedisConstants.CACHE_SHOP_KEY + shop.getId();
        stringRedisTemplate.delete(key);

    }

    /**
     * 查询所有商铺id信息
     *
     * @return
     */
    @Override
    public List<Long> queryAllShopIds() {
        return baseMapper.queryAllShopIds();
    }

    /**
     * 保存商铺信息
     *
     * @param shop
     */
    @Override
    public void saveShop(Shop shop) {
        save(shop);
        String id = shop.getId().toString();
//        shpBloomFilter.add(id);
    }

    /**
     * 根据商铺类型分页查询商铺信息
     *
     * @param typeId
     * @param current
     * @param x
     * @param y
     * @return
     */
    @Override
    public List<Shop> queryShopByType(Long typeId, Integer current, Double x, Double y) {
        // 1.判断 xy是否为空,为空 不需要地理查询
        if (x == null || y == null) {
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
            return page.getRecords();
        }
        //2. 进行分页数据整理
        int from = (current - 1) * SystemConstants.MAX_PAGE_SIZE;
        int end = current * SystemConstants.MAX_PAGE_SIZE;
        //3.查询地理位置信息
        String key = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(
                key,
                GeoReference.fromCoordinate(x, y),
                new Distance(5000),
                RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
                        .includeDistance()
                        .limit(end)
        );
        if (results == null) return Collections.emptyList();
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> resultList = results.getContent();
        if (resultList.size() <= from) {
            return Collections.emptyList();
        }
        //4.组装数据,进行截取
        List<String> shopIds = new ArrayList<>(resultList.size());
        Map<String, Double> distanceMap = new HashMap<>(resultList.size());
        resultList.stream().skip(from).forEach(result -> {

            String shopIdStr = result.getContent().getName();
            double dis = result.getDistance().getValue();
            // 保存shopId,及id:距离映射
            shopIds.add(shopIdStr);
            distanceMap.put(shopIdStr, dis);
        });
        //5. 查询数据库,返回数据
        String idStr = StrUtil.join(",", shopIds);
        List<Shop> list = query().in("id", shopIds).last("ORDER BY FIELD(id," + idStr + ")").list();
        if (CollUtil.isEmpty(list)) return Collections.emptyList();
        // 5.1 填充距离信息
        list.forEach(shop -> shop.setDistance(distanceMap.get(shop.getId().toString())));
        return list;
    }

    /**
     * 互斥锁获取锁
     *
     * @param id
     * @return
     */
    private boolean tryLock(Long id) {
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(RedisConstants.LOCK_SHOP_KEY + id, "1", Duration.ofSeconds(5L));
        return BooleanUtil.isTrue(result);
    }

    /**
     * 释放锁
     *
     * @param id
     */
    private void unLock(Long id) {
        stringRedisTemplate.delete(RedisConstants.LOCK_SHOP_KEY + id);
    }
}
